﻿using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace AI.Graph {


    [System.Serializable]
    public class ParameterReference {

        public ExposedParameter exposedParameter;
        public UnityEngine.Object unityObjectReference;

    }


    [AddComponentMenu ("AI/AI Behaviour", 11)]
    [DisallowMultipleComponent]
    public class AIBehaviour : MonoBehaviour {

#region Fields

        [SerializeField]
        private bool m_startOnAwake = true;

        [SerializeField]
        private AIController m_sharedController = null;

        [NonSerialized]
        private AIController m_runtimeController;

        [NonSerialized]
        private State m_currentState;

        [SerializeField]
        private ParameterReference[] m_parameterReferences = new ParameterReference[0];


        private Dictionary<string, object> m_valuesList = new Dictionary<string, object> ();
        private List<Log> m_debugStack = new List<Log> ();
        private bool m_start;
        private bool m_started;
        private bool m_isPaused;

#endregion


#region Properties

        public Log[] logs {
            get { return m_debugStack.ToArray (); }
        }


        public AIController sharedController {
            get { return m_sharedController; }
        }

        public AIController runtimeController {
            get {
                if (m_runtimeController == null) {
                    if (m_sharedController != null) {
                        m_runtimeController = AIControllerSerializer.Copy (m_sharedController);
                    }
                }

                return m_runtimeController;
            }
        }


        public string[] valueKeys {
            get { return m_valuesList.Keys.ToArray (); }
        }


        public bool started {
            get { return m_started; }
        }

        public bool paused {
            get { return m_isPaused; }
        }

#endregion


#region Unity

        void Start () {
            foreach (Node node in runtimeController.nodes) {
                node.StartNode (this);
            }

            if (m_startOnAwake) {
                StartAI ();
            }
        }


        void Update () {
            if (!m_started && m_start && !m_isPaused) {
                // update entry State
                foreach (ConnectionPort output in runtimeController.entryState.outputs) {
                    if (CheckConnections (output, "[Entry]: Change State")) {
                        break;
                    }
                }
            }
            else {
                // update current State
                if (m_currentState && !m_isPaused) {
                    m_currentState.StateUpdate (this);
                    CheckConnections (m_currentState.output, "Change State");
                }

                // update any State
                foreach (ConnectionPort anyOutput in runtimeController.anyState.outputs) {
                    if (CheckConnections (anyOutput, "[Any]: Change State")) {
                        break;
                    }
                }
            }
        }


        void OnDisable () {
            ChangeState (null, "[Disable]: Change State");
        }

#endregion


#region Internal

        bool CheckConnections (ConnectionPort output, string promt) {
            foreach (ConnectionPort input in output.connectedPorts) {
                Node node = input.node;

                if (node.GetType () == typeof (State) || node.GetType ().IsSubclassOf (typeof (State))) {
                    State nextState = node as State;
                    ChangeState (nextState, promt);
                    return true;
                }

                if (node.GetType () == typeof (Condition) || node.GetType ().IsSubclassOf (typeof (Condition))) {
                    Condition condition = (Condition) node;
                    State nextState = condition.Decide (this);

                    if (nextState) {
                        ChangeState (nextState, promt);
                        return true;
                    }
                }
            }

            return false;
        }


        void ChangeState (State newState, string promt) {
            Log (this, "Change State");
            if (m_currentState) {
                m_currentState.StateExit (this);
            }

            m_currentState = newState;

            if (m_currentState) {
                m_currentState.StateEnter (this);
            }

            m_started = true;
        }

#endregion


        /// <summary>
        /// Starts the ai.
        /// </summary>
        public void StartAI () {
            m_start = true;
            m_isPaused = false;
        }


        /// <summary>
        /// Pause the ai.
        /// </summary>
        public void PauseAI () {
            m_isPaused = true;
        }


        /// <summary>
        /// Resume the ai.
        /// </summary>
        public void ResumeAI () {
            m_isPaused = false;
        }


        /// <summary>
        /// Stop, reset and start the ai.
        /// </summary>
        public void StopAndStartAI () {
            StopAI ();
            StartAI ();
        }


        /// <summary>
        /// Reset and stop the ai.
        /// </summary>
        public void StopAI () {
            ChangeState (null, "[Stop]: Change State");
            m_start = false;
            m_isPaused = false;
            m_started = false;
        }


        /// <summary>
        /// Returns the exposed value for the given exposed parameter.
        /// </summary>
        /// <returns>Returns true if the exposed parameter exists.</returns>
        public bool TryGetExposedObject<T> (ExposedParameter parameter, out T value) where T : UnityEngine.Object {
            for (int i = 0; i < m_parameterReferences.Length; i++) {
                if (m_parameterReferences[i].exposedParameter == parameter) {
                    try {
                        value = (T) m_parameterReferences[i].unityObjectReference;
                        return true;
                    }
                    catch (Exception) {
                        value = default (T);
                        return false;
                    }
                }
            }

            value = default (T);
            return false;
        }


        /// <summary>
        /// Returns true if the exposed parameter exists.
        /// </summary>
        public bool ContainsExposedParameter (ExposedParameter parameter) {
            foreach (ParameterReference reference in m_parameterReferences) {
                if (reference.exposedParameter == parameter) return true;
            }

            return false;
        }


        /// <summary>
        /// Returns the value for the given node parameter.
        /// </summary>
        /// <returns>Returns true if the node parameter exists.</returns>
        public bool TryGetValue<T> (NodeParameter parameter, out T value) {
            return (TryGetValue (parameter.name, out value));
        }


        /// <summary>
        /// Returns the value for the given key.
        /// </summary>
        /// <returns>Returns true if the key exists.</returns>
        public bool TryGetValue<T> (string key, out T value) {
            foreach (string k in m_valuesList.Keys) {
                if (key == k) {
                    try {
                        value = (T) m_valuesList[k];
                        return true;
                    }
                    catch (Exception) {
                        value = default (T);
                        return false;
                    }
                }
            }

            value = default (T);
            return false;
        }


        /// <summary>
        /// Sets the given value to the given node parameter.
        /// </summary>
        /// <returns>Returns true if succeeded.</returns>
        public bool TrySetValue<T> (NodeParameter parameter, T value, bool overrideType = false) {
            return TrySetValue (parameter.name, value, overrideType);
        }


        /// <summary>
        /// Sets the given value to the given key.
        /// </summary>
        /// <returns>Returns true if succeeded.</returns>
        public bool TrySetValue (string key, object value, bool overrideType = false) {
            if (m_valuesList.ContainsKey (key)) {
                object v = m_valuesList[key];
                if (v.GetType () != value.GetType () && !value.GetType ().IsSubclassOf (v.GetType ())) {
                    if (!overrideType) {
                        return false;
                    }
                }

                m_valuesList.Remove (key);
            }

            m_valuesList.Add (key, value);
            return true;
        }


        /// <summary>
        /// Log a message on the Log panel.
        /// Has only an effect in Unity-Editor-Mode.
        /// </summary>
        public void Log (UnityEngine.Object obj, string message) {
            #if UNITY_EDITOR
            bool wasLastUpdate = false;

            for (int i = m_debugStack.Count - 1, j = 0; i >= 0 && j < 5; i--, j++) {
                if (m_debugStack[i].obj == obj && m_debugStack[i].message == message) {
                    Log log = m_debugStack[i];
                    log.count++;
                    m_debugStack[i] = log;
                    wasLastUpdate = true;
                    break;
                }
            }

            if (!wasLastUpdate) {
                m_debugStack.Add (new Log (obj, message));
            }
            #endif
        }

    }


}